package org.jrazdacha.bittorrent.modelling.core;

import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;


/**
 * Object model which represents peer
 * 
 * @author nep
 *
 */
public class Peer {

	private final Communicator communicator;
	private Map<Id, Connection> connections = new HashMap<Id, Connection>();
	private Map<Id, BitSet> bitfields = new HashMap<Id, BitSet>();
	private final Id id;
	private Torrent torrent;

	/**
	 * Constructs new peer
	 * @param id id of peer
	 * @param communicator communicator for sending messages to other peers
	 */
	public Peer(Id id, Communicator communicator) {	
		this.id = id;
		this.communicator = communicator;		
	}

	/**
	 * 
	 * @return id of peer
	 */
	public Id getId() {
		return id;
	}

	/**
	 * Starts seeding of given torrent. This
	 * peer has all pieces of given torrent.
	 * 
	 * @param torrent torrent to seed
	 */
	public void startSeeding(Torrent torrent) {
		this.torrent = torrent;
		torrent.complete();		
	}
	
	/**
	 * Starts leeching of given torrent. This peer
	 * has zero pieces of given torrent.
	 * 
	 * @param torrent torrent to leach
	 */
	public void startLeeching(Torrent torrent) {
		this.torrent = torrent;
	}

	/**
	 * Sends initial handshake for establishing connection
	 * to peer with id = remotePeerId
	 * @param remotePeerId id of remote peer
	 */
	public void sendHandShakeTo(Id remotePeerId) {
		connections.get(remotePeerId).handshakeWasSend();
		communicator.sendHandShakeTo(remotePeerId);		
	}

	/**
	 * Creates not established connection to the remote peer
	 * 
	 * @param remotePeerId id of remote peer
	 * 
	 * @return Connection in state DISCONNECTED
	 */
	public Connection initiateConnectionTo(Id remotePeerId) {		
		Connection connection = new Connection();
		connections.put(remotePeerId, connection);
		return connection;
	}
	
	/**
	 * Returns connection by remote peer id
	 * 
	 * @param remotePeerId
	 * @return
	 */
	public Connection getConnectionTo(Id remotePeerId) {
		return connections.get(remotePeerId);
	}

	/**
	 * Receives handshake from remote peer
	 * @param remotePeerId id of remote peer
	 */
	public void receiveHandShakeFrom(Id remotePeerId) {		
		findOrCreateConnectionTo(remotePeerId).handshakeReceived();		
	}

	private Connection findOrCreateConnectionTo(Id remotePeerId) {		
		if (null != connections.get(remotePeerId)) {
			return connections.get(remotePeerId);			
		} else {			
			return initiateConnectionTo(remotePeerId);
		}		
	}

	/**
	 * Sends bitfield to peer with remotePeerId
	 * 
	 * @param remotePeerId
	 */
	public void sendBitfieldTo(Id remotePeerId) {
		connections.get(remotePeerId).bitfieldWasSend();
		communicator.sendBitfieldTo(torrent.getBitfield(), remotePeerId);		
	}

	/**
	 * Performs next step in establishing connection between
	 * this peer and peers with id's in set of peers
	 * 
	 * @param peers id's of remote peers
	 */
	public void connectTo(Set<Id> peers) {	
		setUpConnectionsForNewPeers(peers);
		
		for (Map.Entry<Id, Connection> connectionEntry : connections.entrySet()) {
			if (ConnectionState.DISCONNECTED.equals(connectionEntry.getValue().getState())) {
				sendHandShakeTo(connectionEntry.getKey());							
			} else if (ConnectionState.RECEIVED_HANDSHAKE.equals(connectionEntry.getValue().getState())) {
				sendHandShakeTo(connectionEntry.getKey());
			} else if (ConnectionState.HANDSHAKE_REPLY_ARRIVED.equals(connectionEntry.getValue().getState())) {
				sendBitfieldTo(connectionEntry.getKey());
			} else if (ConnectionState.BITFIELD_RECEIVED.equals(connectionEntry.getValue().getState())) {
				sendBitfieldTo(connectionEntry.getKey());
			}
		}
	}
	
	private void setUpConnectionsForNewPeers(Set<Id> peers) {
		removeOldPeers(peers);
		for (Id remotePeerId : peers) {
			initiateConnectionTo(remotePeerId);
		}
	}

	private void removeOldPeers(Set<Id> peers) {
		peers.remove(id);
		peers.removeAll(connections.keySet());
	}

	/**
	 * Receives bitfield from peer with id remotePeerId
	 * 
	 * @param bitfield bitfield from remote peer id
	 * @param remotePeerId id of remote peer
	 */
	public void receiveBitfieldFrom(BitSet bitfield, Id remotePeerId) {
		bitfields.put(remotePeerId, bitfield);
		connections.get(remotePeerId).bitfieldReceived();		
	}
	
	/**
	 * Returns bitfield of the remote peer
	 * 
	 * @param remotePeerId id of remote peer
	 * @return bitfield of the remote peer
	 */
	public BitSet getBitfieldForPeer(Id remotePeerId) {		
		return bitfields.get(remotePeerId);
	}

	/**
	 * Receives message from the peer with remotePeerId.
	 * Performs logic dependent from the type of message
	 * 
	 * @param message message from the remote peer
	 * @param remotePeerId id of remote peer 
	 */
	public void receiveMessageFrom(Message message, Id remotePeerId) {
		if (ConversationId.HANDSHAKE.equals(message.getConversationId())) {
			receiveHandShakeFrom(remotePeerId);
		}		
		else if (ConversationId.BITFIELD.equals(message.getConversationId())) {
			receiveBitfieldFrom((BitSet)message.getContent(), remotePeerId);
		}
	}
}
